import { IExponentialBackoffOptions } from '../backoff/ExponentialBackoff';

/**
 * Function used to get the next delay.
 */
export type GeneratorFn<S> = (
  state: S | undefined,
  options: IExponentialBackoffOptions<S>,
) => [number, S];

/**
 * Generator that creates a backoff with no jitter.
 */
export const noJitterGenerator: GeneratorFn<number> = (attempts = 0, options) => [
  Math.min(options.maxDelay, options.initialDelay * 2 ** attempts),
  attempts + 1,
];

/**
 * Generator that randomizes an exponential backoff between [0, delay).
 */
export const fullJitterGenerator: GeneratorFn<number> = (state, options) => {
  const [delay, next] = noJitterGenerator(state, options);
  return [Math.floor(Math.random() * delay), next];
};

/**
 * Generator that randomizes an exponential backoff between [0, delay).
 */
export const halfJitterGenerator: GeneratorFn<number> = (attempts, options) => {
  const [delay, next] = noJitterGenerator(attempts, options);
  return [Math.floor((delay + Math.random() * delay) / 2), next];
};

/**
 * A factor used within the formula to help smooth the first calculated delay.
 */
const pFactor = 4.0;

/**
 *  A factor used to scale the median values of the retry times generated by
 * the formula to be _near_ whole seconds, to aid user comprehension. This
 * factor allows the median values to fall approximately at 1, 2, 4 etc
 * seconds, instead of 1.4, 2.8, 5.6, 11.2.
 */
const rpScalingFactor = 1 / 1.4;

/**
 * Decorrelated jitter. This should be considered the optimal Jitter stategy
 * for most scenarios, as battle-tested in Polly.
 *
 * @see https://github.com/App-vNext/Polly/issues/530
 * @see https://github.com/Polly-Contrib/Polly.Contrib.WaitAndRetry/blob/24cb116a2a320e82b01f57e13bfeaeff2725ccbf/src/Polly.Contrib.WaitAndRetry/Backoff.DecorrelatedJitterV2.cs
 */
export const decorrelatedJitterGenerator: GeneratorFn<[number, number]> = (state, options) => {
  const [attempt, prev] = state || [0, 0];
  const t = attempt + Math.random();
  const next = Math.pow(options.exponent, t) * Math.tanh(Math.sqrt(pFactor * t));
  const formulaIntrinsicValue = Math.max(0, next - prev);
  return [
    Math.min(formulaIntrinsicValue * rpScalingFactor * options.initialDelay, options.maxDelay),
    [attempt + 1, next],
  ];
};
